# --------------------------------------------------------------------------------
#  NoiseTube Web application
#  
#  Copyright (C) 2008-2010 SONY Computer Science Laboratory Paris
#  Portions contributed by Vrije Universiteit Brussel (BrusSense team), 2008-2011
# --------------------------------------------------------------------------------
#  This library is free software; you can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License, version 2.1, as published
#  by the Free Software Foundation.
#  
#  This library is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
#  details.
#  
#  You should have received a copy of the GNU Lesser General Public License along
#  with this library; if not, write to:
#    Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor,
#    Boston, MA  02110-1301, USA.
#  
#  Full GNU LGPL v2.1 text: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
#  NoiseTube project source code repository: http://code.google.com/p/noisetube
# --------------------------------------------------------------------------------
#  More information:
#   - NoiseTube project website: http://www.noisetube.net
#   - Sony Computer Science Laboratory Paris: http://csl.sony.fr
#   - VUB BrusSense team: http://www.brussense.be
# --------------------------------------------------------------------------------
 
require 'builder'
require 'gchart'

class Cartasur::SearchException < Exception;
end

class Cartasur::ApiController < Cartasur::ApplicationController
  #note: always send query with salt

  before_filter :verify_apikey, :only => [:newsession, :update, :upload, :postlog, :endsession, :resumesession, :endallsessions,
                                          :search, :visitedobjectives, :pendingobjectives, :objectives, :photoupload,
                                          ]


  def cartasur_user_pending_objectives

    @user = current_user if (@user.nil?)
    unless @user.nil?
      #ret = "<xml> <error>user not logged in </error> </xml>" if @user.blank?
      #get all protocols
      @all_protocols = Cartasur::Protocol.where(:users_id => @user.id)
      #and all visited ones, if any
      @visiteds = Cartasur::Visited.where(:user_id => @user.id)

      @visiteds_objective_ids = @visiteds.map(&:objective_id)
      pending_protocols = []
      visited_protocols = []
      #remove the visiteds from the pendings
       @all_protocols.map do |protocol|
         if  @visiteds_objective_ids.include?(protocol.objective_id)
             visited_protocols << protocol  #todo: include this
         else
             pending_protocols << protocol
         end
      end

      objectives_to_return = pending_protocols.collect{|protocol| {protocol: protocol, objective: protocol.objective}  unless protocol.nil?}

    end
    objectives_to_return
  end

  def cartasur_user_visited_objectives
    @user = current_user if (@user.nil?)
    unless @user.nil?
      #ret = "<xml> <error>user not logged in </error> </xml>" if @user.blank?
      #get all protocols
      @all_protocols = Cartasur::Protocol.where(:users_id => @user.id)
      #and all visited ones, if any
      @visiteds = Cartasur::Visited.where(:user_id => @user.id)

      @visiteds_objective_ids = @visiteds.map(&:objective_id)
      pending_protocols = []
      visited_protocols = []
      #remove the visiteds from the pendings
      @all_protocols.map do |protocol|
        if  @visiteds_objective_ids.include?(protocol.objective_id)
          visited_protocols << protocol
        end
      end

      objectives_to_return = pending_protocols.collect{|protocol| {protocol: protocol, objective: protocol.objective}  unless protocol.nil?}

    end
    objectives_to_return
  end

  # cartasur_user_all_objectives - gets a user's all objectives as xml
  def cartasur_user_all_objectives

    @user = current_user if (@user.nil?)

    objectives_to_return = []

    unless @user.nil?
      objectives_to_return = @user.all_protocols
    end
    objectives_to_return
  end

  # photoupload - upload photo for a finished protocol
  # at the moment takes only one photo (future: may need multiple upload functionality)
  # requires params :key, :protocol id (or :objective and :user ids, and will search for the protocol itself)
  # and :file (the photo)
  def photoupload

    begin

      ret = "could not save the photo at this time"

      objective_id = params[:objective]
      protocol_id = params[:protocol]

      #identify which user it belongs to
      if params[:key].nil?
        user_id = Protocol.find(protocol_id).user.try(:id)
      else
        #get from params
        user_id = @user.id unless @user.nil?
      end
      #

      #save filename with useful ids
      file_ids = protocol_id ? "_p_"+ protocol_id : "_u_" + user_id + "_o_" + objective_id

      #check if file was sent
      if params[:file].blank?
        ret = "error. did not specify file."
        logger.info("Did not receive photo data.. protocol: " + protocol_id )
      else
        #upload for backwards compatibility and IE
        ajax_upload = params[:file].is_a?(String)

        filename = ajax_upload ? params[:file] : params[:file].original_filename
        extension = filename.split('.').last

        # Creating a temp file
        tmp_file = "#{Rails.root}/public/uploads/tmp"

        # Save to temp file
        File.open(tmp_file, 'wb') do |f|
          if ajax_upload
            f.write request.body.read
          else
            f.write params[:file].read
          end
        end

        my_file = File.open(tmp_file)

        #proto = Protocol.find(protocol_id) unless protocol_id.nil?
        #obj = Objective.find(objective_id) unless objective_id.nil?
        #usr = Objective.find(objective_id) unless objective_id.nil?

        unless protocol_id.blank?
           protocol = Cartasur::Protocol.find(protocol_id)
           user_id, objective_id = protocol.users_id, protocol.objective_id
        end

        #get the visited
        visited = Cartasur::Visited.where("protocol_id = ?", protocol_id)

        visited = Cartasur::Visited.get_by_user_and_objective(user_id, objective_id).first  if visited.nil?

        unless visited.blank?
          vimage = Cartasur::VisitedsImage.new
          vimage.visited_id = visited[0].id
          vimage.image_url =  my_file
          vimage.save!
          ret = "ok. file uploaded."
        end
      end


      logger.info("Cartasur: Saved photo successfully in " <<  tmp_file) unless tmp_file.nil?

    rescue Exception
      ret = $!.message + "<br/>" +$!.backtrace.join("</br>")
      logger.error("Cartasur: Error when saving photo:" + $!.message)
    end
    render :text => ret, :status => "200 OK"
  end

  #------------- reponses --------------
  def visitedobjectives

    @results =  cartasur_user_visited_objectives
    unless @results.nil?
      respond_to do |format|
        format.xml
        format.html {render :text=> 'error. specify request format.', :status => "200 OK"}
      end
    else
      render :text => "no items not found." , :status => "200 OK"
    end
  end


  def pendingobjectives

    @results =  cartasur_user_pending_objectives
    unless @results.nil?
      respond_to do |format|
        format.xml
        format.html {render :text=> 'error. specify request format.', :status => "200 OK"}
      end
    else
      render :text => "no items not found." , :status => "200 OK"
    end
  end


  def objectives

    @results =  cartasur_user_all_objectives
    unless @results.nil?
      respond_to do |format|
        format.xml
        format.html {render :text=> 'error. specify request format.', :status => "200 OK"}
      end
    else
      render :text => "no items found." , :status => "200 OK"
    end
  end

  ############## from base apicontroller ####################
  def index
    redirect_to '/cartasur/'
  end

  def mobilecalibrations
    #Contains hack to deliver alternative calibration file to Android clients with version < v1.2.3 (this file uses the Build.BRAND field for deviceBrand instead of Build.MANUFACTURER)
    androidAgentSlash = 'NoiseTubeMobileAndroid/'
    agent = request.headers['User-Agent']
    if !agent.nil? && agent[0, androidAgentSlash.length] == androidAgentSlash
      androidClientVersion = agent[androidAgentSlash.length, agent.length]
      if androidClientVersion <= "v1.2.2"
        send_file "#{Rails.root}/public/calibrationsAndroidPre123.xml", :type=>"application/xml"
      else
        send_file "#{Rails.root}/public/calibrations.xml", :type=>"application/xml"
      end
    else
      send_file "#{Rails.root}/public/calibrations.xml", :type=>"application/xml"
    end
  end

  # Mobile Authentication
  def authenticate
    user = Cartasur::User.find_by_login(params[:login])
    if user.nil? || !user.authenticated?(params[:password])
      logger.info(" Could not log in user #{params[:login]}.")
      render :json => "error"
    else
      render :json=> user.salt
    end
  end

  # Ping tool
  def ping
    render :text => "ok", :status=>200
  end


  # Start a new measurement session #
  def newsession
    # in case we haven't closed the previous session
    # close_session  # closes the most recent running session

    # create the new track
    track = start_session
    render :text => "ok #{track.id}", :status=>200
  end


  # close a measurement session  #
  def endsession
    track = running_track(false) #do not start a new one
    if close_session(track)
      render :text => "ok", :status=>200
    else
      render :nothing => true, :status=>404 #specified track did no exist, or it was already closed, or there is no running track
    end
  end

  def endallsessions
    #TODO
  end

  def resumesession
    track = running_track(false) #do not start a new one
    unless track.nil?
      render :text => "ok #{track.id}", :status=>200
    else
      render :nothing => true, :status=>404 #specified track did no exist of there is no running track
    end
  end

  # ## Update the sensor request: http://noisetube.net/api/update?time=(timestamp)&db=(decibel)&l={geo:(lat,lon)|placeid}
  # Parameters:
  #   time={time}: the time of the measure (in second) (required)
  #   db=: decibel in db(A) integer (required)
  #   l: geo:{lat,lng}|placeID
  #   placeID= the id of a place e.g. one of your favoraite location (home, office) , the station of a subway
  #   geo coordinates: l=geo:lat,lng
  #   result: response 200 : ok response 500 : ##
  def update
    begin
      track = running_track(true) #start a new one if needed
      track.add_measurement(params)
                                  #render :nothing=> true
      render :text => "ok", :status=>200
    rescue Exception
      render :text=> $!.message + "<br/>" +$!.backtrace.join("</br>")
    end
  end

  # Update the sensor request: http://noisetube.net/api/upload # batch version (through JSON format)
  #{
  #    "key" : "2812f53fdad045089a1ef1614d3a70a7cae84871",
  #    "measure" : [
  #                {
  #                    "db" : "26",
  #                "l" : "geo:50.821452,4.396129fb",
  #                "time" : "2013-03-25T16:40:16Z+01:00"
  #            },
  #                {
  #                    "db" : "27",
  #                "l" : "geo:50.821452,4.396129",
  #                "time" : "2013-03-25T16:40:18Z+01:00"
  #            },...
  #            ]}

  # liveupload - from the noisetube api, as upload
  # live upload of measurements from phone
  # requires parameter :key - the API key of the user
  def liveupload
    track = running_track(true) #start a new one if needed, todo: link this to the user
    begin
      params[:measures].each { |m_params|
        unless m_params.nil?
          #old JSON format (usertags & autotags together)
          time = m_params[:time]  || m_params[0]
          db = m_params[:db]  || m_params[1]
          l = m_params[:l]  || m_params[2]
          tag = m_params[:tag]  || m_params[3]
          #removed autotags as param[4]

          track.add_measurement({
                                    :time=>time,
                                    :db=>db,
                                    :l=>l,
                                    :tag=>tag})
        end
      }
      render :text => "ok #{track.id}", :status=>200

    rescue => ex
      logger.error ex.message
      render :text => "error", :status=>200
    end
  end

  def postlog
    directory = "log/posted"
    # create the file path
    path = File.join(directory, "User#{@user.id}.log")
    # write the file
    File.open(path, 'a') { |f| f.write(params[:log]) }
    render :text => "ok", :status=>200
  end

  # search measures
  def search
    begin
      measures = Cartasur::Measure.search(params, [:user]).collect { |measure|
        {:lat=>measure.geom.lat,
         :lng=>measure.geom.lng,
         :made_at=>measure.made_at,
         :loudness=>measure.loudness,
         :tags=>measure.tags,
         :user=>measure.user.id}
      }

      # send
      render :json=>measures.to_json

    rescue SearchException
      render :text=>"#{$!.message}"
    end
  end


  # ## Tag distribution ##
  def tagdist

    # querying
    labels, freq=Cartasur::Api.distribution_tags(params) #todo: change to cartasur

    respond_to do |format|
      format.png{
        graph=@template.generate_tags_distribution_graph(labels,freq)
        redirect_to graph
      }

      format.json{
        render :json=>[labels, freq].to_json
      }
    end
  end

  def leqdist
    dist=Cartasur::Api.distribution_leq(params)    #todo: change to cartasur

    respond_to do |format|
      # JSON RESPONSE
      format.json{
        dist_label={}
        dist.each_with_index{ |e, i|  dist_label[Cartasur::Measure::DISTRIBUTION_LABEL[i]]=e}
        render :json=>dist_label.to_json
      }

      # PNG RESPONSE
      format.png{
        graph= view_context.generate_leq_distribution_graph(dist,params)
        redirect_to graph
      }
    end
  end

  # verify the API Key
  def verify_apikey
    @user=Cartasur::User.find_by_salt(params[:key])
    if @user.nil?
      render(:text=> "API key not found")
    end
  end

  # renamed from protocolupload to just upload
  # function upload: upload the measurements and protocol answers parsed from an xml document
  # reads params - param[:file] and param[:key]
  def upload
    # get the uploaded file and read it...
    uploaded_file = params[:file]
    xml_data = uploaded_file.read if uploaded_file.respond_to? :read

    res = {:text => "error. request incomplete", :status => "400 Bad Request"}
    # do we have data from the file?
    if request.get? or xml_data.nil?

     logger.warn("Cartasur: Could not upload the file for "+ @user.try(:login) + " - probably no data or not a post request.")
     res = {:text => "error: upload failed. probably no data or not a post request.", :status => "400 Bad Request"}
    else

      # append session closing tag if needed:
      closing_tag = "</NoiseTube-Mobile-Session>"
      xml_data += closing_tag if xml_data.rindex(closing_tag).nil?
      begin
        # parse the data using REXML...
        xml_doc = REXML::Document.new(xml_data)

        #new - 28/05/2013: constraints followed?

        surveyroot = REXML::XPath.first( xml_doc, "//survey")
        # public static final int FOLLOWING_CONSTRAINTS_NO    = 0;
        #public static final int FOLLOWING_CONSTRAINTS_YES   = 1;
        #public static final int FOLLOWING_CONSTRAINTS_MAYBE = 2;
        constraints_followed  = surveyroot.attribute("constraints_followed").try(:value)

        #constraints not followed
         if  constraints_followed.nil? or constraints_followed == "0"
           #directory where to make the move to
           tmp = uploaded_file.tempfile
           file = File.join("public", "uploads", "cartasur", "unprocessed", uploaded_file.original_filename << "-"<< Time.now.to_s(:number))
           #copy to directory
           FileUtils.cp tmp.path, file

           #remove upload, if possible
           #FileUtils.rm tmp
           #ps: i dont create a visited or a track for constraints_followed=0

           res = {:text => "upload ok", :status => "200 OK"}
         else
        #constraints followed but no GPS

          #28/05/2013 - Kennedy - Get the objective gps for missing gps values
          svroot = REXML::XPath.first( xml_doc, "//survey")
          protocol_id = svroot.attribute("protocol_id").try(:value)
          protocol = Cartasur::Protocol.find(protocol_id)

          #check if that protocol exists in visiteds table (has been uploaded already)
          already_uploaded = Cartasur::Visited.where({:protocol_id => protocol.id, :user_id => protocol.user.id}).first

          if already_uploaded.nil?


            #save track first
            track=Cartasur::Track.from_xml(xml_doc, protocol.user, protocol, constraints_followed)

            # log successful
            logger.info "Cartasur: Uploading measurements from xml file uploaded by #{@user.login}..... "
            logger.info "Cartasur: A track id #{track.id} with a total of #{track.measures.size} measures was added at #{Time.now}"

            #save surveys
            Cartasur::Answer.savefromxml(xml_doc)

            #save uploaded xml file - for logging purposes
            tmp = uploaded_file.tempfile
            file = File.join("public", "uploads", "cartasur","processed", uploaded_file.original_filename << "-"<< Time.now.to_s(:number))
            #copy to directory
            FileUtils.cp tmp.path, file

            #create a visited
            @visited = Cartasur::Visited.new({protocol_id: protocol_id, track_id: track.id, user_id: protocol.user.id, objective_id: protocol.objective.id})
            if @visited.save
              #done...
              res = {:text => "upload ok", :status => "200 OK"}
              logger.info "Cartasur: Successfully uploaded measurements by #{@user.try(:login)}"
            else
              raise "Could not save the visited info for upload by user #{@user.id}"
            end


          else
              #alredy uploaded - copy to duplicates folder

              tmp = uploaded_file.tempfile

              file = File.join("public", "uploads", "cartasur", "duplicates", uploaded_file.original_filename << "-"<< Time.now.to_s(:number))
              dir = File.dirname(file)
              FileUtils.mkdir_p(dir) unless File.directory?(dir)    #create dir

              #copy to directory
              FileUtils.cp tmp.path, file

              #ps: i dont create a visited or a track for duplicates

              res = {:text => "upload ok", :status => "200 OK"}
          end

        end

      rescue Exception => exc
        logger.error("Cartasur: Error processing uploaded track (XML file): #{exc.message}")
        res= {:text => "Error when reading uploaded file: #{exc.message}", :status => "400 Bad Request"}

      end

      render res
    end

  end

  #------------ testing the upload functionality -----------------
  #test upload functionality of cartasur app
  # added: 24-10-2013
  def testupload
    # get the uploaded file and read it...
    uploaded_file = params[:file]

    if uploaded_file.size > 5048576
      res = {:text => "error. request file too large (> 10MB)", :status => "400 Bad Request"}
    else

      xml_data = uploaded_file.read if uploaded_file.respond_to? :read

      res = {:text => "error. request incomplete", :status => "400 Bad Request"}
      # do we have data from the file?
      if request.get? or xml_data.nil?

        logger.warn("Cartasur TEST: Could not upload the file for "+ @user.try(:login) + " - probably no data or not a post request.")
        res = {:text => "error: upload failed. probably no data or not a post request.", :status => "400 Bad Request"}
      else

        # append session closing tag if needed:
        closing_tag = "</NoiseTube-Mobile-Session>"
        xml_data += closing_tag if xml_data.rindex(closing_tag).nil?
        begin
          # parse the data using REXML...
          xml_doc = REXML::Document.new(xml_data)

          #new - 28/05/2013: constraints followed?

          surveyroot = REXML::XPath.first( xml_doc, "//survey")
          # public static final int FOLLOWING_CONSTRAINTS_NO    = 0;
          #public static final int FOLLOWING_CONSTRAINTS_YES   = 1;
          #public static final int FOLLOWING_CONSTRAINTS_MAYBE = 2;
          constraints_followed  = surveyroot.attribute("constraints_followed").try(:value)

          #constraints not followed
          if  constraints_followed.nil? or constraints_followed == "0"
            #directory where to make the move to
            tmp = uploaded_file.tempfile
            file = File.join("public", "uploads", "cartasur", "test", "unprocessed", "test_file-" << uploaded_file.original_filename << "-"<< Time.now.to_s(:number))
            #copy to directory
            FileUtils.cp tmp.path, file

            #remove upload, if possible
            #FileUtils.rm tmp
            #ps: i dont create a visited or a track for constraints_followed=0

            res = {:text => "upload ok. check uploads directory at /cartasur/api/testuploaddir", :status => "200 OK"}
          else
            #constraints followed but no GPS

            #28/05/2013 - Kennedy - Get the objective gps for missing gps values
            svroot = REXML::XPath.first( xml_doc, "//survey")


            # log successful
            logger.info "Cartasur TEST: Uploading xml file ..... "

            #save uploaded xml file - for logging purposes
            tmp = uploaded_file.tempfile
            file = File.join("public", "uploads", "cartasur","test", "processed", "test_file-" << Time.now.to_s(:number) << "-" << uploaded_file.original_filename )
            #copy to directory
            FileUtils.cp tmp.path, file

              #done...
            res = {:text => "upload of test file ok. check the file using /cartasur/api/testuploaddir", :status => "200 OK"}
            logger.info "Cartasur TEST: Successfully uploaded TEST file"
          end

        rescue Exception => exc
          logger.error("Cartasur TEST Error processing uploaded track (XML file): #{exc.message}")
          res= {:text => "Error when reading uploaded file: #{exc.message}", :status => "400 Bad Request"}

        end

      end
    end
    render res
  end

  def testuploaddir
    res = "<h3>Listing for uploaded files</h3> "

    if params[:dl]
      path = "#{Rails.root}/public/uploads/cartasur/test/#{params[:dl]}"
      if File.exists?(path)  and params[:p] == "1"
        send_file( path, x_sendfile: true )
      else
        res = "<b>not found</b>"
      end
    else
      @dirs = Dir.glob(File.join("public", "uploads", "cartasur","test","*"))

    for dir in @dirs
      res << "Directory: <b>" << dir[/\/.*/] <<"</b>"
      @files = Dir.glob(dir << "/*")
      @files.sort_by{ |f| File.ctime f }
      res << "<ul>"
      for file in @files
        dl = "unprocessed/" << File.basename(file)
        dl = "processed/" << File.basename(file) if dir !~ /unprocessed/
        res << "<li> "<<  "<a href='testuploaddir?dl=#{dl}&p=1'  > #{File.basename(file)} </a>  (Created: #{File.mtime(file)})" << "</li>"
      end
      res << "</ul><br />"
    end



    render :inline => res, :status => "200 OK"
    end
  end

  def unprocessedfiles      #todo: test this
    res = "<h3>Listing for unprocessed files</h3> "

    if params[:dl]
      path = "#{Rails.root}/public/uploads/cartasur/test/#{params[:dl]}"
      if File.exists?(path)  and params[:p] == "1"
        send_file( path, x_sendfile: true )
      else
        res = "<b>not found</b>"
      end
    else
      @dirs = Dir.glob(File.join("public", "uploads", "cartasur","*"))

      for dir in @dirs
        if dir !~ /test/
          res << "Directory: <b>" << dir[/\/.*/] <<"</b>"
          @files = Dir.glob(dir << "/*")
          @files.sort_by{ |f| File.ctime f }
          res << "<ul>"
          for file in @files
            dl = "unprocessed/" << File.basename(file)
            dl = "processed/" << File.basename(file) if dir !~ /unprocessed/
            res << "<li> "<<  "<a href='unprocessedfiles?dl=#{dl}&p=1'  > #{File.basename(file)} </a>  | Created: #{File.mtime(file)} " << "</li>"
          end
          res << "</ul><br />"
        end
      end
      render :inline => res, :status => "200 OK"
    end
  end

  #################

  # -----------------------------------------------------------
  private #!!!



  def most_recent_running_track
    return @user.tracks.find(:last, :conditions=>"ends_at IS NULL", :order=>"starts_at asc")
  end

  # get specified or most recent running track (or a new one if allowed)
  def running_track(start_new_if_none=false)
    track = @user.tracks.find_by_id(params[:track]) unless params[:track].blank? #try to find track by id (if id was specified)
    track = most_recent_running_track if (track.nil? or not track.ends_at.nil?) #no specified track (or it does not exist, or is already closed) --> try most recent running track
    track = start_session if (track.nil? and start_new_if_none) #or start a new one if permitted
    return track
  end

  def close_session(track=most_recent_running_track)
    unless track.nil?
      last_measurement = track.measures.find(:last, :order=>"made_at asc")
      unless last_measurement.nil?
        track.ends_at = last_measurement.made_at
        track.save
      else
        track.destroy  #no last measurement --> no measurements at all --> delete empty track
      end
      return true
    else
      return false
    end
  end

  # create a new session for a given user
  def start_session
    start_time = Time.now #default
    start_time = DateTime.parse(params[:starts_at]) unless params[:starts_at].blank?
    track = Cartasur::Track.create(:user=>@user)
    track.starts_at = start_time #will be overwritten with timestamp (made_at) of first measurement during postprocessing
    track.client = params[:client]
    track.client_version = params[:clientversion]
    track.device = "#{params[:devicebrand]} #{params[:devicemodel]}"
                          # TODO...
    track.save
    return track
  end


end
